Domine o tratamento de erros `useActionState` no React. Aprenda uma estratégia completa para recuperação de erros, preservando a entrada do usuário e criando formulários resilientes para um público global.
Recuperação de Erros com `useActionActionState` no React: Uma Estratégia Abrangente de Tratamento de Erros de Ação
No mundo do desenvolvimento web, a experiência do usuário de um formulário é um ponto de contato crítico. Um formulário contínuo e intuitivo pode levar a uma conversão bem-sucedida, enquanto um frustrante pode fazer com que os usuários abandonem uma tarefa completamente. Com a introdução das Server Actions e do novo hook useActionState no React 19, os desenvolvedores têm ferramentas poderosas para gerenciar envios de formulários e transições de estado. No entanto, simplesmente exibir uma mensagem de erro quando uma ação falha não é mais suficiente.
Uma aplicação verdadeiramente robusta antecipa falhas e fornece um caminho claro para a recuperação do usuário. O que acontece quando uma conexão de rede cai? Ou quando a entrada de um usuário falha na validação do lado do servidor? O usuário perde todos os dados que acabou de passar minutos digitando? É aqui que uma estratégia sofisticada de tratamento e recuperação de erros se torna essencial.
Este guia abrangente levará você além do básico de useActionState. Exploraremos uma estratégia completa para lidar com erros de ação, preservar a entrada do usuário e criar formulários resilientes e fáceis de usar que funcionam de forma confiável para um público global. Passaremos da teoria para a implementação prática, construindo um sistema que seja poderoso e mantenível.
O que é `useActionState`? Uma Breve Revisão
Antes de mergulharmos em nossa estratégia de recuperação, vamos revisitar brevemente o hook useActionState (que era conhecido como useFormState em versões experimentais anteriores do React). Seu propósito principal é gerenciar o estado de uma ação de formulário, incluindo estados de pendência e os dados retornados pelo servidor.
Ele simplifica um padrão que anteriormente exigia uma combinação de useState, useEffect e gerenciamento manual de estado para lidar com envios de formulários.
A sintaxe básica é a seguinte:
const [state, formAction, isPending] = useActionState(action, initialState);
action: A função de ação do servidor a ser executada. Esta função recebe o estado anterior e os dados do formulário como argumentos.initialState: O valor que você deseja que o estado tenha inicialmente, antes que a ação seja chamada.state: O estado retornado pela ação após sua conclusão. Na renderização inicial, este é oinitialState.formAction: Uma nova ação que você passa para a propactiondo seu elemento<form>. Quando esta ação é invocada, ela acionará aactionoriginal, atualizará o sinalizadorisPendinge atualizará ostatecom o resultado.isPending: Um booleano que étrueenquanto a ação está em andamento efalsecaso contrário. Isso é incrivelmente útil para desativar botões de envio ou exibir indicadores de carregamento.
Embora este hook seja um primitivo fantástico, seu verdadeiro poder é desbloqueado quando você projeta um sistema robusto em torno dele.
O Desafio: Além da Simples Exibição de Erros
A implementação mais comum de tratamento de erros com useActionState envolve a ação do servidor retornando um objeto de erro simples, que é então exibido na UI. Por exemplo:
// Uma ação de servidor simples, mas limitada
export async function updateUser(prevState, formData) {
const name = formData.get('name');
if (name.length < 3) {
return { success: false, message: 'O nome deve ter pelo menos 3 caracteres.' };
}
// ... atualiza o usuário no banco de dados
return { success: true, message: 'Perfil atualizado!' };
}
Isso funciona, mas tem limitações significativas que levam a uma experiência de usuário ruim:
- Entrada do Usuário Perdida: Quando o formulário é enviado e ocorre um erro, o navegador re-renderiza a página com o resultado renderizado pelo servidor. Se os campos de entrada não forem controlados, qualquer dado que o usuário inseriu pode ser perdido, forçando-o a começar de novo. Esta é uma fonte primária de frustração para o usuário.
- Nenhum Caminho de Recuperação Claro: O usuário vê uma mensagem de erro, mas e agora? Se houver vários campos, eles não sabem qual está incorreto. Se for um erro de servidor, eles não sabem se devem tentar novamente agora ou mais tarde.
- Incapacidade de Diferenciar Erros: O erro foi devido a entrada inválida (um erro de nível 400), uma falha no lado do servidor (um erro de nível 500) ou uma falha de autenticação? Uma simples string de mensagem não pode transmitir esse contexto, que é crucial para construir respostas de UI inteligentes.
Para construir aplicações profissionais de nível empresarial, precisamos de uma abordagem mais estruturada e resiliente.
Uma Estratégia Robusta de Recuperação de Erros com `useActionState`
Nossa estratégia é construída sobre três pilares fundamentais: uma resposta de ação padronizada, gerenciamento inteligente de estado no cliente e uma UI centrada no usuário que guia a recuperação.
Etapa 1: Definindo um Formato de Resposta de Ação Padronizado
Consistência é a chave. A primeira etapa é estabelecer um contrato - uma estrutura de dados consistente que toda ação de servidor retornará. Essa previsibilidade permite que nossos componentes frontend lidem com o resultado de qualquer ação sem lógica personalizada para cada uma.
Aqui está um formato de resposta robusto que pode lidar com uma variedade de cenários:
// Uma definição de tipo para nossa resposta padronizada
interface ActionResponse<T> {
success: boolean;
message?: string; // Para feedback global voltado para o usuário (por exemplo, notificações de toast)
errors?: Record<string, string[]> | null; // Erros de validação específicos do campo
errorType?: 'VALIDATION' | 'SERVER_ERROR' | 'AUTH_ERROR' | 'NOT_FOUND' | null;
data?: T | null; // A carga útil em caso de sucesso
}
success: Um booleano claro indicando o resultado.message: Uma mensagem global e legível por humanos. Isso é perfeito para toasts ou banners como "Perfil atualizado com sucesso" ou "Não foi possível conectar ao servidor.".errors: Um objeto onde as chaves correspondem aos nomes dos campos do formulário (por exemplo,'email') e os valores são arrays de strings de erro. Isso permite exibir vários erros por campo.errorType: Uma string semelhante a um enum que categoriza o erro. Este é o molho secreto que permite que nossa UI reaja de forma diferente a diferentes modos de falha.data: O recurso criado ou atualizado com sucesso, que pode ser usado para atualizar a UI ou redirecionar o usuário.
Exemplo de Resposta de Sucesso:
{
success: true,
message: 'Perfil do usuário atualizado com sucesso!',
data: { id: '123', name: 'John Doe', email: 'john.doe@example.com' }
}
Exemplo de Resposta de Erro de Validação:
{
success: false,
message: 'Por favor, corrija os erros abaixo.',
errors: {
email: ['Por favor, insira um endereço de e-mail válido.'],
password: ['A senha deve ter pelo menos 8 caracteres.', 'A senha deve conter um número.']
},
errorType: 'VALIDATION'
}
Exemplo de Resposta de Erro de Servidor:
{
success: false,
message: 'Ocorreu um erro inesperado. Nossa equipe foi notificada. Por favor, tente novamente mais tarde.',
errors: null,
errorType: 'SERVER_ERROR'
}
Etapa 2: Projetando o Estado Inicial do Componente
Com nosso formato de resposta definido, o estado inicial passado para useActionState deve espelhá-lo. Isso garante consistência de tipo e evita erros em tempo de execução ao acessar propriedades que não existem na renderização inicial.
const initialState = {
success: false,
message: '',
errors: null,
errorType: null,
data: null
};
Etapa 3: Implementando a Ação do Servidor
Agora, vamos implementar uma ação de servidor que adere ao nosso contrato. Usaremos a popular biblioteca de validação zod para demonstrar o tratamento limpo de erros de validação.
'use server';
import { z } from 'zod';
// Define o esquema de validação
const profileSchema = z.object({
name: z.string().min(3, { message: 'O nome deve ter pelo menos 3 caracteres.' }),
email: z.string().email({ message: 'Por favor, insira um endereço de e-mail válido.' }),
});
// A ação do servidor adere à nossa resposta padronizada
export async function updateUserProfileAction(previousState, formData) {
const validatedFields = profileSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
});
// Lida com erros de validação
if (!validatedFields.success) {
return {
success: false,
message: 'Falha na validação. Por favor, verifique os campos.',
errors: validatedFields.error.flatten().fieldErrors,
errorType: 'VALIDATION',
data: null
};
}
try {
// Simula uma operação de banco de dados
console.log('Atualizando usuário:', validatedFields.data);
// const updatedUser = await db.user.update(...);
// Simula um possível erro de servidor
if (validatedFields.data.email.includes('fail')) {
throw new Error('Falha na conexão com o banco de dados');
}
return {
success: true,
message: 'Perfil atualizado com sucesso!',
errors: null,
errorType: null,
data: validatedFields.data
};
} catch (error) {
console.error('Erro do Servidor:', error);
return {
success: false,
message: 'Ocorreu um erro interno do servidor. Por favor, tente novamente mais tarde.',
errors: null,
errorType: 'SERVER_ERROR',
data: null
};
}
}
Esta ação é agora uma função previsível e robusta. Ela separa claramente a lógica de validação da lógica de negócios e lida com erros inesperados de forma graciosa, sempre retornando uma resposta que nosso frontend pode entender.
Construindo a UI: Uma Abordagem Centrada no Usuário
Agora para a parte mais importante: usar esse estado estruturado para criar uma experiência de usuário superior. Nosso objetivo é guiar o usuário, não apenas bloqueá-lo.
Configuração do Componente Principal
Vamos configurar nosso componente de formulário. A chave para preservar a entrada do usuário em caso de falha é usar componentes controlados. Gerenciaremos o estado das entradas com useState. Quando o envio do formulário falhar, o componente será re-renderizado, mas como os valores das entradas estão em estado do React, eles não são perdidos.
'use client';
import { useState } from 'react';
import { useActionState } from 'react';
import { updateUserProfileAction } from './actions';
const initialState = { success: false, message: '', errors: null, errorType: null };
export function UserProfileForm({ user }) {
const [state, formAction, isPending] = useActionState(updateUserProfileAction, initialState);
// Usa useState para controlar as entradas do formulário e preservá-las na re-renderização
const [name, setName] = useState(user.name);
const [email, setEmail] = useState(user.email);
return (
<form action={formAction}>
Editar Perfil
{/* Banner de Mensagem de Erro/Sucesso Global */}
{state.message && (
<div style={{ color: state.success ? 'green' : 'red' }}>
{state.message}
</div>
)}
<div>
<label htmlFor="name">Nome</label>
<input
id="name"
name="name"
value={name}
onChange={(e) => setName(e.target.value)}
aria-invalid={!!state.errors?.name}
aria-describedby="name-error"
/>
{state.errors?.name && (
<p id="name-error" style={{ color: 'red' }}>{state.errors.name[0]}</p>
)}
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
aria-invalid={!!state.errors?.email}
aria-describedby="email-error"
/>
{state.errors?.email && (
<p id="email-error" style={{ color: 'red' }}>{state.errors.email[0]}</p>
)}
</div>
<button type="submit" disabled={isPending}
>
{isPending ? 'Salvando...' : 'Salvar Alterações'}
</button>
</form>
);
}
Principais Detalhes de Implementação da UI:
- Entradas Controladas: Ao usar
useStateparanameeemail, os valores das entradas são gerenciados pelo React. Quando a ação do servidor falha e o componente é re-renderizado com o novo estado de erro, as variáveis de estadonameeemailpermanecem inalteradas, preservando assim a entrada do usuário perfeitamente. Esta é a técnica mais importante para uma boa experiência de recuperação. - Banner de Mensagem Global: Usamos
state.messagepara mostrar uma mensagem de nível superior. Podemos até mudar sua cor com base emstate.success. - Erros Específicos do Campo: Verificamos
state.errors?.fieldNamee, se presente, renderizamos a mensagem de erro diretamente abaixo da entrada relevante. - Acessibilidade: Usamos
aria-invalidpara indicar programaticamente aos leitores de tela que um campo tem um erro.aria-describedbyvincula a entrada à sua mensagem de erro, garantindo que o texto do erro seja lido quando o usuário foca no campo inválido. - Estado de Pendência: O booleano
isPendingé usado para desativar o botão de envio, impedindo envios duplicados e fornecendo feedback visual claro de que uma operação está em andamento.
Padrões Avançados de Recuperação
Com nossa base sólida, agora podemos implementar experiências de usuário mais avançadas com base no tipo de erro.
Lidando com Diferentes Tipos de Erro
Nosso campo errorType agora é incrivelmente útil. Podemos usá-lo para renderizar componentes de UI completamente diferentes para diferentes cenários de falha.
function ErrorRecoveryUI({ state, onRetry }) {
if (!state.errorType) return null;
switch (state.errorType) {
case 'VALIDATION':
// Para validação, o feedback principal são os erros de campo em linha,
// então podemos não precisar de um componente especial aqui. A mensagem global é suficiente.
return <p style={{ color: 'orange' }}>Por favor, revise os campos marcados em vermelho.</p>;
case 'SERVER_ERROR':
return (
<div style={{ border: '1px solid red', padding: '1rem' }}>
<h3>Ocorreu um Erro de Servidor</h3>
<p>{state.message}</p>
<button onClick={onRetry} type="button">Tentar Novamente</button>
</div>
);
case 'AUTH_ERROR':
return (
<div style={{ border: '1px solid red', padding: '1rem' }}>
<h3>Sessão Expirada</h3>
<p>Sua sessão expirou. Por favor, faça login novamente para continuar.</p>
<a href="/login">Ir para Login</a>
</div>
);
default:
return <p style={{ color: 'red' }}>{state.message}</p>;
}
}
// No retorno do seu componente principal:
<form action={formAction}>
{/* ... campos do formulário ... */}
<ErrorRecoveryUI state={state} onRetry={() => { /* lógica para reativar o formulário */ }} />
<button type="submit" disabled={isPending}>Salvar</button>
</form>
Implementando um Mecanismo de "Tentar Novamente"
Para erros recuperáveis como SERVER_ERROR, um botão "Tentar Novamente" é uma excelente UX. Como implementamos isso? O `formAction` está vinculado ao evento de envio do formulário. Uma abordagem simples é fazer com que o botão "Tentar Novamente" redefina o estado da ação e reative o formulário, convidando o usuário a clicar novamente no botão de envio principal.
Como useActionState não fornece uma função `reset`, um padrão comum é envolvê-lo em um hook personalizado ou gerenciá-lo fazendo o componente re-renderizar com uma nova chave, embora muitas vezes a abordagem mais simples seja apenas guiar o usuário.
Uma solução pragmática: A entrada do usuário já está preservada. O sinalizador `isPending` estará falso. O melhor "tentar novamente" é simplesmente permitir que o usuário clique no botão de envio original novamente. A UI pode simplesmente guiá-lo:
Para um `SERVER_ERROR`, nossa UI pode mostrar a mensagem de erro: "Ocorreu um erro. Suas alterações foram salvas. Por favor, tente enviar novamente." O botão de envio já está ativado porque `isPending` é falso. Isso não requer gerenciamento complexo de estado.
Combinando com `useOptimistic`
Para uma sensação ainda mais responsiva, useActionState combina maravilhosamente com o hook useOptimistic. Você pode assumir que a ação terá sucesso e atualizar a UI instantaneamente. Se a ação falhar, useActionState receberá o estado de erro, o que acionará uma re-renderização e reverterá automaticamente a atualização otimista para o estado real.
Isso está além do escopo deste mergulho profundo no tratamento de erros, mas é o próximo passo lógico na criação de experiências verdadeiramente modernas com React Actions.
Considerações Globais para Aplicações Internacionais
Ao construir para um público global, codificar mensagens de erro em inglês não é uma opção viável.
Internacionalização (i18n)
Nossa estrutura de resposta padronizada pode ser facilmente adaptada para internacionalização. Em vez de retornar uma string de `message` codificada, o servidor deve retornar uma chave ou código de mensagem.
Resposta de Servidor Modificada:
{
success: false,
messageKey: 'errors.validation.checkFields',
errors: {
email: ['errors.validation.email.invalid'],
},
errorType: 'VALIDATION'
}
No cliente, você usaria uma biblioteca como react-i18next ou react-intl para traduzir essas chaves para o idioma selecionado pelo usuário.
import { useTranslation } from 'react-i18next';
// Dentro do seu componente
const { t } = useTranslation();
// ...
{state.messageKey && <p>{t(state.messageKey)}</p>}
// ...
{state.errors?.email && <p>{t(state.errors.email[0])}</p>}
Isso desacopla sua lógica de ação da camada de apresentação, tornando sua aplicação mais fácil de manter e traduzir para novos idiomas.
Conclusão
O hook useActionState é mais do que apenas uma conveniência; é uma peça fundamental para a construção de aplicações web modernas e resilientes no React. Ao ir além da exibição básica de mensagens de erro e adotar uma estratégia abrangente de recuperação de erros, você pode melhorar drasticamente a experiência do usuário.
Vamos recapitular os principais princípios de nossa estratégia:
- Padronize a Resposta do Seu Servidor: Crie uma estrutura JSON consistente para todas as suas ações. Este contrato é a base de um comportamento previsível no frontend. Inclua um
errorTypedistinto para diferenciar os modos de falha. - Preserve a Entrada do Usuário a Todo Custo: Use componentes controlados (
useState) para gerenciar os valores dos campos do formulário. Isso evita a perda de dados em falhas de envio e é a base de uma experiência de usuário tolerante. - Forneça Feedback Contextual: Use seu estado de erro estruturado para exibir mensagens globais, erros de campo em linha e UI personalizada para diferentes tipos de erro (por exemplo, erros de validação vs. erros de servidor).
- Construa para um Público Global: Desacople as mensagens de erro da lógica do seu servidor usando chaves de internacionalização e sempre considere os padrões de acessibilidade (atributos ARIA) para garantir que seus formulários sejam utilizáveis por todos.
Ao investir em uma estratégia robusta de tratamento de erros, você não está apenas corrigindo bugs - você está construindo confiança com seus usuários. Você está criando aplicações que parecem estáveis, profissionais e respeitosas com o tempo e o esforço deles. À medida que você continua a construir com React Actions, deixe esta estrutura guiá-lo na criação de experiências que não são apenas funcionais, mas verdadeiramente agradáveis de usar, não importa onde seus usuários estejam no mundo.